/*
  author Sylvain Bertrand <digital.ragnarok@gmail.com>
  Protected by GNU Affero GPL v3 with some exceptions.
  See README at root of alga tree.
*/
#include <linux/module.h>
#include <linux/device.h>
#include <linux/i2c.h>
#include <linux/slab.h>
#include <asm/unaligned.h>

#include <alga/alga.h>
#include <alga/timing.h>
#include <alga/edid.h>

#include "vesa_dmts.h"

/* NOTE: do not even try to understand the code without the EDID specs */

#define I2C_FETCH_RETRIES 4

static bool has_valid_hdr(void *edid)
{
	static u8 valid_hdr[8] = {
		0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00};
	if (memcmp(edid, valid_hdr, sizeof(valid_hdr)) != 0)
		return false;
	return true;
}

static bool is_v1_4(void *edid)
{
	u8 version;
	u8 revision;

	version = *(u8*)(edid + 0x12);
	if (version != 1)
		return false;

	revision = *(u8*)(edid + 0x13);
	if (revision != 4)
		return false;
	return true;
}

static int timings_add(struct alga_timing *t, struct alga_timing **ts)
{
	unsigned i;
	struct alga_timing *new;

	i = 0;
	while ((*ts)[i].h != 0) /* get to the terminator */
		++i;

	new = krealloc(*ts, sizeof(*t) * (i + 2), GFP_KERNEL);
	if (new == NULL)
		return -ALGA_ERR;
	*ts = new;

	(*ts)[i + 1].h = 0; /* terminator */
	(*ts)[i] = *t; /* insert the new mode at the end */
	return 0;
}

static int dtd_collect(void *dtd, struct alga_timing **ts)
{
	struct alga_timing t;
	u8 *p;
	unsigned tmp;

	p = (u8*)dtd;

	if ((p[17] & 0x80) != 0) /* do not support interlaced modes */
		return 0;
	
	t.pixel_clk = get_unaligned_le16(dtd) * 10;

	tmp = p[2];
	tmp |= (p[4] & 0xf0) << 4;
	t.h = tmp;

	tmp = p[3];
	tmp |= (p[4] & 0x0f) << 8;
	t.h_bl = tmp;

	tmp = p[5];
	tmp |= (p[7] & 0xf0) << 4;
	t.v = tmp;

	tmp = p[6];
	tmp |= (p[7] & 0x0f) << 8;
	t.v_bl = tmp;

	/* tmp is front porch from blanking */
	tmp = p[8];
	tmp |= (p[11] & 0xc0) << 2;
	t.h_so = t.h + tmp;

	tmp = p[9];
	tmp |= (p[11] & 0x30) << 4;
	t.h_spw = tmp;

	/* tmp is front porch from blanking */
	tmp = (p[10] & 0xf0) >> 4;
	tmp |= (p[11] & 0x0c) << 2;
	t.v_so = t.v + tmp;

	tmp = p[10] & 0x0f; 
	tmp |= (p[11] & 0x03) << 4;
	t.v_spw = tmp;

	/* only digital separated sync supported */
	t.h_sp = 0;
	t.v_sp = 0;
	if ((p[17] & 0x0e) == 0x0c)
		t.v_sp = 1;
	if ((p[17] & 0x09) == 0x08)
		t.h_sp = 1;
	return timings_add(&t, ts);
}

static int dtds_collect(void *edid, struct alga_timing **ts)
{
	unsigned i;
	void *dtd;

	for (i = 0, dtd = edid + 0x36; i < 4; ++i, dtd += 18) {
		u16 pixel_clk;

		pixel_clk = get_unaligned_le16(dtd);
		if (pixel_clk != 0)
			dtd_collect(dtd, ts);
	}
	return 0;
}


static int timings(void *edid, struct alga_timing **ts)
{
	int r;

	r = dtds_collect(edid, ts);
	if (r != 0)
		return r;
	/* TODO: collect the modes defined in other ways */
	return 0;
}

int alga_edid_timings(struct device *dev, void *edid, struct alga_timing **ts)
{
	int r;

	if (!is_v1_4(edid)) {
		dev_err(dev, "alga: edid version not supported\n");
		return -ALGA_ERR;
	}
	
	/* init the array, namely only the terminator */
	*ts = kzalloc(sizeof(**ts), GFP_KERNEL);
	if (*ts == NULL)
		return -ALGA_ERR;
	
	r = timings(edid, ts);
	if (r != 0) {
		kfree(*ts);
		*ts = NULL;
	}
	return r;
}
EXPORT_SYMBOL_GPL(alga_edid_timings);

int alga_i2c_edid_fetch(struct device *dev, struct i2c_adapter *a, void **edid)
{
	u8 out;
	int r;
	int retry;
	struct i2c_msg msgs[] = {
		{
			.addr = 0x50,
			.flags = 0,
			.len = 1,
			.buf = &out
		},
		{
			.addr = 0x50,                                                                                                                                                             
			.flags = I2C_M_RD,                                                                                                                                                        
			.len = EDID_BLK_SZ,
			.buf = NULL
		}
	};

	*edid = kzalloc(EDID_BLK_SZ, GFP_KERNEL);
	if (*edid == NULL) {
		dev_err(dev, "alga: unable to allocate memory for EDID\n");
		return -ALGA_ERR;
	}

	msgs[1].buf = *edid;
	out = 0; /* block 0 */

	for (retry = 0; retry < I2C_FETCH_RETRIES; ++retry) {
		r = i2c_transfer(a, msgs, 2);                                                                                                                 
		if (r == 2)
			break;
	}
	if (r != 2) {
		dev_err(dev, "alga: unable to fetch the edid over i2c\n");
		r = -ALGA_ERR;
		goto free_edid;
	}

	if (!has_valid_hdr(*edid)) {
		dev_err(dev, "alga: invalid edid header\n");
		r = -ALGA_ERR;
		goto free_edid;
	}
	return 0;

free_edid:
	kfree(*edid);
	*edid = NULL;	
	return r;
}
EXPORT_SYMBOL_GPL(alga_i2c_edid_fetch);

int alga_edid_bpc(struct device *dev, void *edid)
{
	u8 def;

	def = *(u8*)(edid + 0x14);

	if ((def & BIT(7)) == 0) {
		dev_warn(dev, "alga: edid video input definition is not "
								"digital\n");
		return 0;
	}

	def &= 0x70;

	switch (def) {
	case 0x10:
		return 6;
	case 0x20:
		return 8;
	case 0x30:
		return 10;
	case 0x40:
		return 12;
	case 0x50:
		return 14;
	case 0x60:
		return 16;
	case 0x70:
	case 0x00:
	default:
		return 0;
	}
}
EXPORT_SYMBOL_GPL(alga_edid_bpc);
